/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, ts, AstTreeUtils } from "arkanalyzer/lib";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import Logger, { LOG_MODULE_TYPE } from "arkanalyzer/lib/utils/logger";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../Index";
import { RuleFix } from "../../model/Fix";

const logger = Logger.getLogger(
  LOG_MODULE_TYPE.HOMECHECK,
  "SemiCheck"
);

const gMetaData: BaseMetaData = {
  severity: 2,
  ruleDocPath: "docs/semi-check.md",
  description: "Require or disallow semicolons instead of ASI"
};

export class SemiCheck implements BaseChecker {
  readonly metaData: BaseMetaData = gMetaData;
  public rule: Rule;
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  private arkFile: ArkFile;
  private sourceFile: ts.SourceFile;
  private sourceCode: string;
  private sourceCodeLength: number;
  private static readonly whitespaceRegex = /\s/;
  private static readonly commentRegex = /\/\*[\s\S]*?\*\/|\/\/.*/g;
  private static readonly statementContinuationChars = new Set(['[', '(', '/', '+', '-', ',', '.', '*', '`']);

  private options = {
    semi: "always",
    omitLastInOneLineBlock: false,
    omitLastInOneLineClassBody: false,
    beforeStatementContinuationChars: "any"
  };

  private fileMatcher: FileMatcher = {
    matcherType: MatcherTypes.FILE
  };

  private nodeCache: Map<ts.Node, {
    isOneLineBlock?: boolean,
    lineInfo?: { line: number, character: number },
    hasSemicolon?: boolean
  }> = new Map();
  
  private visitedNodes: Set<ts.Node> = new Set();
  private blockCache: Map<ts.Node, boolean> = new Map();
  private lineCache: Map<number, number> = new Map();

  registerMatchers(): MatcherCallback[] {
    const matchFileCb: MatcherCallback = {
      matcher: this.fileMatcher,
      callback: this.check.bind(this)
    };

    return [matchFileCb];
  }

  public check(arkFile: ArkFile): void {
    this.arkFile = arkFile;
    this.sourceCode = arkFile.getCode();
    this.sourceCodeLength = this.sourceCode.length;
    this.sourceFile = AstTreeUtils.getASTNode(arkFile.getFilePath(), this.sourceCode);

    this.nodeCache.clear();
    this.visitedNodes.clear();
    this.blockCache.clear();
    this.lineCache.clear();

    if (this.rule && this.rule.option) {
      if (typeof this.rule.option[0] === "string") {
        this.options.semi = this.rule.option[0];
      }
      if (typeof this.rule.option[1] === "object") {
        this.options = { ...this.options, ...this.rule.option[1] };
      }
    }

    this.visitNode(this.sourceFile);
  }

  private getNodeInfo(node: ts.Node) {
    let info = this.nodeCache.get(node);
    if (!info) {
      info = {};
      this.nodeCache.set(node, info);
    }
    return info;
  }

  private getLineAndCharacter(pos: number): { line: number, character: number } {
    const cached = this.lineCache.get(pos);
    if (cached !== undefined) {
      return { line: cached, character: pos - this.getLineStart(cached) };
    }
    const { line, character } = this.sourceFile.getLineAndCharacterOfPosition(pos);
    this.lineCache.set(pos, line);
    return { line, character };
  }

  private getLineStart(line: number): number {
    return this.sourceFile.getLineStarts()[line];
  }

  private visitNode(node: ts.Node): void {
    if (ts.isWhileStatement(node)) {
      const statement = node.statement;
      if (ts.isBlock(statement)) {
        statement.statements.forEach(stmt => {
          if (this.isStatement(stmt)) {
            this.checkSemicolon(stmt);
          }
        });
      }
      return;
    }

    if (ts.isIfStatement(node)) {
      const statement = node.thenStatement;
      if (ts.isBlock(statement)) {
        const sourceCode = this.arkFile.getCode();
        const sourceFile = statement.getSourceFile();
        const blockStartPos = statement.getStart();
        const blockEndPos = statement.getEnd();
        const blockStartLine = sourceFile.getLineAndCharacterOfPosition(blockStartPos).line;
        const blockEndLine = sourceFile.getLineAndCharacterOfPosition(blockEndPos).line;
        const isRealOneLineBlock = blockStartLine === blockEndLine;

        if (isRealOneLineBlock && this.options.omitLastInOneLineBlock && this.options.semi === "always") {
          const lastStmt = statement.statements[statement.statements.length - 1];
          const stmtEndPos = lastStmt.getEnd();
          const hasSemicolon = sourceCode[stmtEndPos - 1] === ';';
          if (hasSemicolon) {
            this.report(lastStmt, "Extra semicolon.");
          }
          return;
        }

        for (let i = 0; i < statement.statements.length; i++) {
          const stmt = statement.statements[i];
          const isLastStmt = i === statement.statements.length - 1;
          const stmtEndPos = stmt.getEnd();
          const hasSemicolon = sourceCode[stmtEndPos - 1] === ';';

          if (this.options.semi === "always") {
            if (isLastStmt && isRealOneLineBlock && this.options.omitLastInOneLineBlock) {
              if (hasSemicolon) {
                this.report(stmt, "Extra semicolon.");
              }
            } else {
              if (!hasSemicolon) {
                this.report(stmt, "Missing semicolon.");
              }
            }
          } else if (this.options.semi === "never") {
            const nextCharacter = this.getNextCharacter(stmtEndPos, sourceCode);
            const isASIDangerous = this.isStatementContinuationChar(nextCharacter);

            if (hasSemicolon) {
              if (this.options.beforeStatementContinuationChars === "always" && isASIDangerous) {
                continue;
              }
              
              if (this.options.beforeStatementContinuationChars === "never" ||
                  (this.options.beforeStatementContinuationChars === "any" && !isASIDangerous)) {
                this.report(stmt, "Extra semicolon.");
              }
            }
          }
        }
      }
      return;
    }

    if (ts.isExpressionStatement(node)) {
      this.checkSemicolon(node);
    } else if (ts.isBlock(node)) {
      const sourceCode = this.arkFile.getCode();
      const sourceFile = node.getSourceFile();
      const blockStartPos = node.getStart();
      const blockEndPos = node.getEnd();
      const blockStartLine = sourceFile.getLineAndCharacterOfPosition(blockStartPos).line;
      const blockEndLine = sourceFile.getLineAndCharacterOfPosition(blockEndPos).line;
      const isRealOneLineBlock = blockStartLine === blockEndLine;

      if (isRealOneLineBlock && this.options.omitLastInOneLineBlock && this.options.semi === "always") {
        const lastStmt = node.statements[node.statements.length - 1];
        const stmtEndPos = lastStmt.getEnd();
        const hasSemicolon = sourceCode[stmtEndPos - 1] === ';';
        if (hasSemicolon) {
          this.report(lastStmt, "Extra semicolon.");
        }
        return;
      }

      node.statements.forEach(stmt => {
        if (this.isStatement(stmt)) {
          this.checkSemicolon(stmt);
        }
      });
      
      if (!ts.isWhileStatement(node.parent) && this.isStatement(node)) {
        this.checkSemicolon(node);
      }
    } else if (ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)) {
      const statement = node.statement;
      if (ts.isBlock(statement)) {
        statement.statements.forEach(stmt => {
          if (this.isStatement(stmt)) {
            this.checkSemicolon(stmt);
          }
        });
      } else if (this.isStatement(statement)) {
        this.checkSemicolon(statement);
      }
      this.visitNode(statement);
    } else if (ts.isClassDeclaration(node)) {
      node.members.forEach(member => {
        if (ts.isPropertyDeclaration(member) || 
            (ts.isMethodDeclaration(member) && !member.body)) {
          this.checkSemicolon(member);
        } else if (ts.isMethodDeclaration(member)) {
          if (member.body) {
            this.visitNode(member.body);
          }
        }
      });
    } else if (ts.isFunctionDeclaration(node)) {
      const modifiers = node.modifiers;
      if (modifiers && modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword)) {
        this.checkSemicolon(node);
      } else if (node.body) {
        this.visitNode(node.body);
      }
    } else if (ts.isMethodDeclaration(node)) {
      if (node.body) {
        this.visitNode(node.body);
      }
    } else if (ts.isClassStaticBlockDeclaration(node)) {
      if (node.body) {
        this.visitNode(node.body);
      }
    } else if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
      if (node.body) {
        if (ts.isBlock(node.body)) {
          this.visitNode(node.body);
        } else {
          if (ts.isExpressionStatement(node.body)) {
            this.checkSemicolon(node.body);
          }
        }
      }
    } else if (this.isStatement(node)) {
      this.checkSemicolon(node);
    }

    ts.forEachChild(node, child => this.visitNode(child));
  }

  private isStatement(node: ts.Node): boolean {
    if (ts.isBlock(node)) {
      const parent = node.parent;
      if (ts.isWhileStatement(parent)) {
        return false;
      }
    }

    if (ts.isInterfaceDeclaration(node)) {
      const modifiers = node.modifiers;
      if (modifiers && modifiers.some(mod => 
        mod.kind === ts.SyntaxKind.ExportKeyword && 
        modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword))) {
        return false;
      }
    }

    if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
      const modifiers = node.modifiers;
      if (modifiers && modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword)) {
        return true;
      }
    }

    return ts.isExpressionStatement(node) ||
           ts.isVariableStatement(node) ||
           ts.isReturnStatement(node) ||
           ts.isBreakStatement(node) ||
           ts.isContinueStatement(node) ||
           ts.isThrowStatement(node) ||
           ts.isImportDeclaration(node) ||
           ts.isExportDeclaration(node) ||
           ts.isDebuggerStatement(node) ||
           ts.isEmptyStatement(node) ||
           ts.isDoStatement(node) ||
           ts.isWhileStatement(node) ||
           ts.isExportAssignment(node) ||
           ts.isImportEqualsDeclaration(node) ||
           ts.isTypeAliasDeclaration(node) ||
           (ts.isMethodDeclaration(node) && !node.body) ||
           (ts.isPropertyDeclaration(node) && !node.initializer) ||
           ts.isInterfaceDeclaration(node) ||
           ts.isModuleDeclaration(node);
  }

  private checkSemicolon(node: ts.Node): void {
    if (this.visitedNodes.has(node)) {
      return;
    }
    this.visitedNodes.add(node);

    const nodeEnd = node.getEnd();
    const info = this.getNodeInfo(node);
    
    if (info.hasSemicolon === undefined) {
      info.hasSemicolon = this.sourceCode[nodeEnd - 1] === ';';
    }

    if (ts.isBlock(node)) {
      const parent = node.parent;
      if (ts.isWhileStatement(parent)) {
        return;
      }
    }

    const sourceCode = this.arkFile.getCode();
    const hasSemicolon = info.hasSemicolon;

    if (ts.isBlock(node.parent) && ts.isWhileStatement(node.parent.parent)) {
      const blockText = sourceCode.substring(node.parent.getStart(), node.parent.getEnd());
      if (!blockText.includes('\n')) {
        if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
          if (this.options.semi === "never" && hasSemicolon) {
            this.report(node, "Extra semicolon.");
          } else if (this.options.semi === "always" && !hasSemicolon) {
            this.report(node, "Missing semicolon.");
          }
          return;
        }
      }
    }

    if (ts.isBlock(node.parent)) {
      const blockText = sourceCode.substring(node.parent.getStart(), node.parent.getEnd());
      if (!blockText.includes('\n')) {
        const parentParent = node.parent.parent;
        if (parentParent && (
          ts.isForStatement(parentParent) ||
          ts.isForInStatement(parentParent) ||
          ts.isForOfStatement(parentParent) ||
          ts.isWhileStatement(parentParent) ||
          ts.isFunctionDeclaration(parentParent)
        )) {
          const block = node.parent as ts.Block;
          if (block.statements[block.statements.length - 1] === node) {
            if (this.options.semi === "never" && hasSemicolon) {
              this.report(node, "Extra semicolon.");
            } else if (this.options.semi === "always" && !hasSemicolon) {
              this.report(node, "Missing semicolon.");
            }
          } else {
            if (this.options.semi === "never" && hasSemicolon) {
              this.report(node, "Extra semicolon.");
            } else if (this.options.semi === "always" && !hasSemicolon) {
              this.report(node, "Missing semicolon.");
            }
          }
          return;
        }
      }
    }

    if (ts.isExpressionStatement(node)) {
      const expr = node.expression;
      if ((ts.isPrefixUnaryExpression(expr) || ts.isPostfixUnaryExpression(expr)) &&
          (expr.operator === ts.SyntaxKind.PlusPlusToken || expr.operator === ts.SyntaxKind.MinusMinusToken)) {
        const operatorText = expr.operator === ts.SyntaxKind.PlusPlusToken ? '++' : '--';
        const nodeText = sourceCode.substring(node.getStart(), nodeEnd);
        const hasOperandBefore = nodeText.indexOf(operatorText) > 0;
        const hasOperandAfter = nodeText.indexOf(operatorText) < nodeText.length - 2;
        
        if (hasOperandBefore || hasOperandAfter) {
          const nextChar = this.getNextCharacter(nodeEnd, sourceCode);
          if (!nextChar || SemiCheck.whitespaceRegex.test(nextChar)) {
            if (this.options.semi === "never" && hasSemicolon) {
              this.report(node, "Extra semicolon.");
            } else if (this.options.semi === "always" && !hasSemicolon) {
              this.report(node, "Missing semicolon.");
            }
            return;
          }
        }
      }
    }

    const nextCharacter = this.getNextCharacter(nodeEnd, sourceCode);
    const isASIDangerous = this.isStatementContinuationChar(nextCharacter);

    if (!info.lineInfo) {
      info.lineInfo = this.getLineAndCharacter(nodeEnd);
    }
    const currentLine = info.lineInfo.line;
    
    const nextNonWhitespace = this.getNextNonWhitespaceChar(nodeEnd, sourceCode);
    const nextCharPos = sourceCode.indexOf(nextNonWhitespace, nodeEnd);
    
    if (nextCharPos !== -1) {
      const nextLine = this.getLineAndCharacter(nextCharPos).line;
      if (currentLine === nextLine && 
          !this.isCommentStart(nextNonWhitespace, sourceCode.charAt(nextCharPos + 1)) &&
          !this.isStatementContinuationChar(nextNonWhitespace)) {
        return;
      }
    }

    if (this.options.semi === "always") {
      if (!hasSemicolon) {
        if (this.options.omitLastInOneLineBlock && this.isLastInOneLineBlock(node)) {
          return;
        }
        if (this.options.omitLastInOneLineClassBody && this.isLastInOneLineClassBody(node)) {
          return;
        }
        this.report(node, "Missing semicolon.");
      }
    } else if (this.options.semi === "never") {
      if (hasSemicolon) {
        if (this.options.beforeStatementContinuationChars === "always" && isASIDangerous) {
          return;
        }
        if (this.options.beforeStatementContinuationChars === "never" ||
            (this.options.beforeStatementContinuationChars === "any" && !isASIDangerous)) {
          this.report(node, "Extra semicolon.");
        }
      } else if (isASIDangerous && this.options.beforeStatementContinuationChars === "always") {
        this.report(node, "Missing semicolon.");
      }
    }
  }

  private isLastInOneLineBlock(node: ts.Node): boolean {
    const parent = node.parent;
    if (!parent) {
      return false;
    }

    if (!ts.isBlock(parent) && !(ts.isClassStaticBlockDeclaration && ts.isClassStaticBlockDeclaration(parent))) {
      return false;
    }

    let statements: ts.NodeArray<ts.Statement> | undefined;
    if (ts.isBlock(parent)) {
      statements = parent.statements;
    } else if (ts.isClassStaticBlockDeclaration(parent)) {
      statements = parent.body.statements;
    }

    if (!statements || statements[statements.length - 1] !== node) {
      return false;
    }

    const blockStart = parent.getStart();
    const blockEnd = parent.getEnd();
    const blockText = this.sourceCode.slice(blockStart, blockEnd);
    
    return !blockText.replace(SemiCheck.commentRegex, '').trim().includes("\n");
  }

  private isLastInOneLineClassBody(node: ts.Node): boolean {
    const parent = node.parent;
    if (!parent || !ts.isClassDeclaration(parent)) {
      return false;
    }

    const members = parent.members;
    if (members[members.length - 1] !== node) {
      return false;
    }

    const classStart = parent.getStart();
    const classEnd = parent.getEnd();
    const classText = this.sourceCode.slice(classStart, classEnd);
    
    return !classText.replace(SemiCheck.commentRegex, '').trim().includes("\n");
  }

  private getNextCharacter(position: number, sourceCode: string): string {
    let i = position;
    while (i < this.sourceCodeLength) {
      while (i < this.sourceCodeLength && SemiCheck.whitespaceRegex.test(sourceCode[i])) {
        i++;
      }
      
      if (i < this.sourceCodeLength - 1) {
        const char = sourceCode[i];
        const nextChar = sourceCode[i + 1];
        
        if (char === '/') {
          if (nextChar === '/') {
            i = sourceCode.indexOf('\n', i);
            if (i === -1) break;
            i++;
            continue;
          }
          if (nextChar === '*') {
            i = sourceCode.indexOf('*/', i + 2);
            if (i === -1) break;
            i += 2;
            continue;
          }
        }
      }
      
      if (i < this.sourceCodeLength) {
        return sourceCode[i];
      }
    }
    return "";
  }

  private isStatementContinuationChar(char: string): boolean {
    if (!char) return false;
    
    if (char === '+' || char === '-') {
      const charIndex = this.sourceCode.indexOf(char);
      const nextChar = this.sourceCode[charIndex + 1];
      if ((char === '+' && nextChar === '+') || (char === '-' && nextChar === '-')) {
        const prevChar = this.sourceCode[charIndex - 1];
        const afterChar = this.sourceCode[charIndex + 2];
        
        return (!prevChar || SemiCheck.whitespaceRegex.test(prevChar)) && 
               (!afterChar || SemiCheck.whitespaceRegex.test(afterChar));
      }
    }
    
    return SemiCheck.statementContinuationChars.has(char);
  }

  private isCommentStart(char: string, nextChar: string): boolean {
    return (char === '/' && (nextChar === '/' || nextChar === '*'));
  }

  private getNextNonWhitespaceChar(position: number, sourceCode: string): string {
    let i = position;
    while (i < this.sourceCodeLength && SemiCheck.whitespaceRegex.test(sourceCode[i])) {
      i++;
    }
    return sourceCode[i] || "";
  }

  private report(node: ts.Node, message: string): void {
    const endPos = node.getEnd();
    const { line, character } = this.sourceFile.getLineAndCharacterOfPosition(endPos);
    let column = character;
    
    if (this.options.semi === "always" || this.options.beforeStatementContinuationChars === "always") {
      if (message.includes("Missing")) {
        column = character + 1;
      }
    } else if (this.options.semi === "never") {
      if (message.includes("Extra")) {
        column = character;
      }
    }
    const severity = this.rule.alert ?? this.metaData.severity;
    const defect = new Defects(
      line + 1,
      column,
      column + 1,
      message,
      severity,
      this.rule.ruleId,
      this.arkFile.getFilePath(),
      this.metaData.ruleDocPath,
      true, 
      false,
      false 
    );

    let fix: RuleFix | undefined;
    if (message.includes("Missing")) {
      fix = { range: [endPos, endPos], text: ";" };
    } else if (message.includes("Extra")) {
      fix = { range: [endPos - 1, endPos], text: "" };
    }

    const issue = new IssueReport(defect, fix);
    this.issues.push(issue);
    RuleListUtil.push(defect);
  }
}